﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Foundation.ObjectHydrator.Generators;
using Foundation.ObjectHydrator.Interfaces;

namespace Foundation.ObjectHydrator
{
    public class Hydrator<T>
    {
        Type typeOfT = null;
        IDictionary<string, Mapping> PropertyMap;
        //object[] Args = null;

        public Hydrator()
        {
            typeOfT = typeof(T);
            //Args = args;
            PropertyMap = new Dictionary<string, Mapping>();
            InferProperties();
        }

        /// <summary>
        /// GetSingle returns a single instance of T populated with default, random data.
        /// </summary>
        /// <returns>An instance of Type T.</returns>
        public T GetSingle()
        {
            //T instance = (T)Activator.CreateInstance(typeOfT, Args);
            T instance = (T)Activator.CreateInstance(typeOfT);
            Populate(instance);

            return instance;
        }

        /// <summary>
        /// Returns a generic IList of type T populated with default, random data.
        /// </summary>
        /// <param name="size">The length of the IList to return.</param>
        /// <returns>A generic IList of Type T containing populated entities.</returns>
        public IList<T> GetList(int size)
        {
            if (size < 1)
            {
                throw new ArgumentOutOfRangeException("size", "size must be provided");
            }

            T instance;

            List<T> toReturn;

            toReturn = new List<T>();

            for (int i = 0; i < size; i++)
            {
                instance = GetSingle();
                Populate(instance);
                toReturn.Add(instance);
            }

            return toReturn;
        }

        /// <summary>
        /// Applies a random Integer generator to the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the random Integer to.</param>
        /// <param name="minimum">The minimum Integer that should be provided by the random Integer.</param>
        /// <param name="maximum">The maximum Integer that should be provided by the random Integer.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithInteger(string propertyName, int minimum, int maximum)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new IntegerGenerator(minimum, maximum));

            return this;
        }

        /// <summary>
        /// Applies a random DateTime selection to the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the random Integer to.</param>
        /// <param name="minimum">The minimum DateTime that should be provided.</param>
        /// <param name="maximum">The maximum DateTime that should be provided.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithDate(string propertyName, DateTime minimum, DateTime maximum)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new DateTimeGenerator(minimum, maximum));

            return this;
        }

        /// <summary>
        /// Applies a randomly chosen Double value to the provided Property Name.
        /// By default a value between 0.0 and 100.0 is randomly chosen.
        /// </summary>
        /// <param name="propertyName">The Property to apply the random Double to.</param>
        /// <param name="decimalPlaces">The number of decimal places that the selected Double may have.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithDouble(string propertyName, int decimalPlaces)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new DoubleGenerator(decimalPlaces));

            return this;
        }

        /// <summary>
        /// Applies a randomly chosen Double value to the provided Property Name.
        /// By default a value with 2 decimal places will be randomly chosen.
        /// </summary>
        /// <param name="propertyName">The Property to apply the random Double to.</param>
        /// <param name="minimum">The minimum Double that should be provided.</param>
        /// <param name="maximum">The maximum Double that should be provided.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithDouble(string propertyName, double minimum, double maximum)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new DoubleGenerator(minimum, maximum));

            return this;
        }

        /// <summary>
        /// Applies a randomly chosen Double value to the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the random Double to.</param>
        /// <param name="minimum">The minimum Double that should be provided.</param>
        /// <param name="maximum">The maximum Double that should be provided.</param>
        /// <param name="decimalPlaces">The number of decimal places that the selected Double may have.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithDouble(string propertyName, double minimum, double maximum, int decimalPlaces)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new DoubleGenerator(minimum, maximum, decimalPlaces));

            return this;
        }

        /// <summary>
        /// Applies a provided default value for the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the Default to.</param>
        /// <param name="defaultValue">The value to set the Property to.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithDefault(string propertyName, Object defaultValue)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new DefaultGenerator(defaultValue));

            return this;

        }

        /// <summary>
        /// Applies a randomly populated Entity of the Type provided to the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the random generated Entity to.</param>
        /// <param name="childObjectType">The Type of Child Object to instantiate, populate and apply.</param>
        /// <param name="parameters">The parameters necessary to generate the Child Entity.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithChildEntity(string propertyName, Type childObjectType, object[] parameters)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new TypeGenerator(childObjectType, parameters));

            return this;
        }

        /// <summary>
        /// Applies a randomly selected Phone Number (999) 999-9999 to the provided Property Name.

        /// </summary>
        /// <param name="propertyName">The Property to apply the random Phone Number to.</param>

        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithPhone(string propertyName)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new AmericanPhoneGenerator());

            return this;

        }

        /// <summary>
        /// Applies a randomly selected Postal Code 99999 to the provided Property Name.
        /// Optionally, Zip +4 Extensions will be randomly applied.
        /// </summary>
        /// <param name="propertyName">The Property to apply the random Postal Code to.</param>
        /// <param name="percentageWithExtensions">A percentage between 0 and 100 of numbers that should have Extensions.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithPostalCode(string propertyName, int percentageWithPlusFour)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new AmericanPostalCodeGenerator(percentageWithPlusFour));

            return this;

        }

        /// <summary>
        /// Applies a random selection from the provided enumValues for the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the randomly selected Enum to.</param>
        /// <param name="enumValues">The Array of System.Enum values from which to chose a random entry to return.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> WithEnum(string propertyName, Array enumValues)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new EnumGenerator(enumValues));

            return this;

        }

        /// <summary>
        /// Applies a random selection from the passed list to the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the randomly selected List Item to.</param>
        /// <param name="list">The IEnumerable<![CDATA[<Object>]]> to randomly choose a value from.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> FromList(string propertyName, IEnumerable<Object> list)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new ListGenerator(list));

            return this;
        }

        /// <summary>
        /// Uses the provided IGenerator to populate values in the provided Property Name.
        /// </summary>
        /// <param name="propertyName">The Property to apply the provided generator to.</param>
        /// <param name="generator">an IGenerator to produce values for the Property Name.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> FromGenerator(string propertyName, IGenerator generator)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, generator);

            return this;
        }

        /// <summary>
        /// Provides an empty value for the specified Property Name.
        /// </summary>
        /// <param name="propertyName">The Property name to not populate.</param>
        /// <returns>This instance of the Hydrator for type T.</returns>
        public Hydrator<T> Ignoring(string propertyName)
        {
            PropertyInfo propertyInfo = typeOfT.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            // Check to see if we have this property...
            if (propertyInfo == null)
            {
                throw new ArgumentException("The Property can not be found.", propertyName);
            }

            if (!propertyInfo.CanWrite)
            {
                throw new ArgumentException("The Property can not be written.", propertyName);
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new NullGenerator());

            return this;
        }

        private void Populate(object instance)
        {
            foreach (Mapping mapping in PropertyMap.Values)
            {
                PropertyInfo propertyInfo = instance.GetType().GetProperty(mapping.PropertyName, BindingFlags.Public | BindingFlags.Instance);
                if (propertyInfo != null)
                {
                    propertyInfo.SetValue(instance, mapping.Generator.Generate(), null);
                }
            }
        }

        private void InferProperties()
        {
            foreach (PropertyInfo propertyInfo in typeOfT.GetProperties())
            {
                if (propertyInfo.CanWrite)
                {
                    if (propertyInfo.PropertyType == typeof(DateTime))
                    {
                        InferDateTime(propertyInfo);
                    }

                    if (propertyInfo.PropertyType == typeof(String))
                    {
                        InferString(propertyInfo);
                    }

                    if (propertyInfo.PropertyType == typeof(Int32) ||
                        propertyInfo.PropertyType == typeof(int))
                    {
                        InferInteger(propertyInfo);
                    }

                    if (propertyInfo.PropertyType == typeof(double) ||
                        propertyInfo.PropertyType == typeof(Double))
                    {
                        InferDouble(propertyInfo);
                    }

                    if (propertyInfo.PropertyType.IsEnum)
                    {
                        InferEnum(propertyInfo);
                    }
                }

            }
        }

        private void InferDouble(PropertyInfo propertyInfo)
        {
            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new DoubleGenerator());
        }

        private void InferInteger(PropertyInfo propertyInfo)
        {
            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new IntegerGenerator());
        }

        private void InferString(PropertyInfo propertyInfo)
        {
            if (
                (propertyInfo.Name.ToLower() == "firstname") ||
                (propertyInfo.Name.ToLower() == "fname")
                )
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new FirstNameGenerator());
                return;
            }

            if (
                (propertyInfo.Name.ToLower() == "lastname") ||
                (propertyInfo.Name.ToLower() == "lname")
                )
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new LastNameGenerator());
                return;
            }


            if (propertyInfo.Name.ToLower().Contains("addressline") ||
            (propertyInfo.Name.ToLower().Contains("address"))
            )
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new AmericanAddressGenerator());
                return;
            }

            if (propertyInfo.Name.ToLower().Contains("city")
         )
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new AmericanCityGenerator());
                return;
            }

            if (propertyInfo.Name.ToLower().Contains("state")
)
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new AmericanStateGenerator());
                return;
            }

            if (
                (propertyInfo.Name.ToLower() == "company") ||
                (propertyInfo.Name.ToLower() == "business")
                )
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new CompanyNameGenerator());
                return;
            }

            if (propertyInfo.Name.ToLower().Contains("descri"))
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new TextGenerator(25));
                return;
            }

            if (propertyInfo.Name.ToLower().Contains("phone"))
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new AmericanPhoneGenerator());
                return;
            }

            if (propertyInfo.Name.ToLower().Contains("zip") ||
                propertyInfo.Name.ToLower().Contains("postal"))
            {
                PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new AmericanPostalCodeGenerator(25));
                return;
            }

            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new TextGenerator(50));
        }

        private void InferDateTime(PropertyInfo propertyInfo)
        {
            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new DateTimeGenerator());
        }

        private void InferEnum(PropertyInfo propertyInfo)
        {
            PropertyMap[propertyInfo.Name] = new Mapping(propertyInfo, new EnumGenerator(Enum.GetValues(propertyInfo.PropertyType)));
        }

    }

}
